<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <title>Tests for CSSOM-View Smooth-Scroll DOM API Methods and MSD Animation</title>
  <style>
    #scroll_behavior_test_body {
      width: 100000px;
      height: 100000px;
    }
    .scroll_to_target {
      position: absolute;
      left: 20000px;
      top: 10000px;
      width: 200px;
      height: 200px;
      background-color: rgb(0, 0, 255);
    }
  </style>
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <script src="/tests/SimpleTest/paint_listener.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
  <script type="application/javascript">

  SimpleTest.waitForExplicitFinish();

  function clamp(val, minVal, maxVal) {
    return Math.max(minVal, Math.min(maxVal, val));
  }

  window.addEventListener("load", function(event) {
    if (event.target != document)
      return;

    // See bug 1062609 - these tests do not work with APZ yet. If APZ is
    // enabled, end the tests early.
    if (SpecialPowers.getBoolPref("layers.async-pan-zoom.enabled")) {
      todo(false, "This test does not yet work with APZ.");
      SimpleTest.finish();
      return;
    }

    SpecialPowers.pushPrefEnv(
      { 'set': [['layout.css.scroll-behavior.enabled', true]] },
      function () {
        testScrollBehaviorInterruption(function() {
          testScrollBehaviorFramerate(function() {
            window.scrollTo(0,0);
            SimpleTest.finish();
          });
        });
      }
    );
  }, false);


  function testScrollBehaviorInterruption(nextTest) {
    // Take control of refresh driver
    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0);
    waitForAllPaintsFlushed(function() {

      window.scrollTo(10, 9);
      ok(window.scrollX == 10 && window.scrollY == 9,
        "instant scroll-behavior must be synchronous when setting initial position");

      window.scrollTo(15, 16);
      ok(window.scrollX == 15 && window.scrollY == 16,
        "instant scroll-behavior must be synchronous when setting new position");

      window.scrollTo({left: 100, top: 200, behavior: 'smooth'});
      ok(window.scrollX == 15 && window.scrollY == 16,
        "smooth scroll-behavior must be asynchronous");

      SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(100);
      waitForAllPaintsFlushed(function() {
        ok(window.scrollX != 15 && window.scrollY != 16
           && window.scrollX != 100 && window.scrollY != 200,
          "smooth scroll-behavior must be triggered by window.scrollTo");

        window.scrollTo(50, 52);
        ok(window.scrollX == 50 && window.scrollY == 52,
          "instant scroll-behavior must interrupt smooth scroll-behavior animation");

        SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(100);
        waitForAllPaintsFlushed(function() {
          ok(window.scrollX == 50 && window.scrollY == 52,
            "smooth scroll-behavior animation must stop after being interrupted");

          // Release control of refresh driver
          SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
          waitForAllPaintsFlushed(nextTest);
        });
      });
    });
  }

  function testScrollBehaviorFramerate(nextTest) {
    /**
     *  CSSOM-View scroll-behavior smooth scroll animations must produce the
     *  same results indendently of frame-rate:
     *
     *   - Reference samples of scroll position for each frame are captured from
     *     a smooth scroll at 120fps for variations in X-Distance, Y-Distance.
     *   - Test samples are captured from an animation with the same parameters
     *     at varying framerates.
     *   - Variance in position at each sampled interval is compared to the
     *     120fps reference.  To pass the test, the position of each test
     *     sample must match the reference position with a tolerance of one test
     *     sample frame's range of motion.  This range of motion is calculated
     *     by the position delta of the reference samples one test frame duration
     *     before and after.
     *   - The duration of the reference sample animation and the test sample
     *     animation must match within 1 frame to pass the test.
     *   - The simulation driving the animation must converge and stop on the
     *     destination position for the test to pass.
     */

    // Use 120hz for reference samples
    var referenceFrameRate = 120;

    var frameRates = [ 13, 60 ];
    var deltas = [ {x: 0, y: 0},
                   {x: 1, y: 100},
                   {x: -100, y: 50000} ];

    var deltaIndex = 0;

    function testDeltas() {
      if(deltaIndex >= deltas.length) {
        nextTest();
        return;
      }
      var deltaX = deltas[deltaIndex].x;
      var deltaY = deltas[deltaIndex].y;
      deltaIndex++;

      // startX and startY must be at least as big as the greatest negative
      // number in the deltas array in order to prevent the animation from
      // being interrupted by scroll range boundaries.
      var startX = 1000;
      var startY = 1000;
      var endX = startX + deltaX;
      var endY = startY + deltaY;
      var referenceTimeStep = Math.floor(1000 / referenceFrameRate);

      sampleAnimation(startX, startY, endX, endY,
                      referenceTimeStep, function(refSamples) {

        var referenceDuration = refSamples.length * referenceTimeStep; // ms

        var frameRateIndex=0;

        function testFrameRate() {
          if(frameRateIndex < frameRates.length) {
            var frameRate = frameRates[frameRateIndex++];
            var testTimeStep = Math.floor(1000 / frameRate);

            sampleAnimation(startX, startY, endX, endY,
                            testTimeStep, function(testSamples) {
              var testDuration = testSamples.length * testTimeStep; // ms

              // Variance in duration of animation must be accurate to within one
              // frame interval
              var durationVariance = Math.max(0, Math.abs(testDuration - referenceDuration) - testTimeStep);
              is(durationVariance, 0, 'Smooth scroll animation duration must not '
                 + 'be framerate dependent at deltaX: ' + deltaX + ', deltaY: '
                 + deltaY + ', frameRate: ' + frameRate + 'fps');

              var maxVariance = 0;
              testSamples.forEach(function(sample, sampleIndex) {

                var testToRef = refSamples.length / testSamples.length;
                var refIndexThisFrame = clamp(Math.floor(sampleIndex * testToRef),
                                              0, refSamples.length - 1);
                var refIndexPrevFrame = clamp(Math.floor((sampleIndex - 1) * testToRef),
                                              0, refSamples.length - 1);
                var refIndexNextFrame = clamp(Math.floor((sampleIndex + 1) * testToRef),
                                              0, refSamples.length - 1);

                var refSampleThisFrame = refSamples[refIndexThisFrame];
                var refSamplePrevFrame = refSamples[refIndexPrevFrame];
                var refSampleNextFrame = refSamples[refIndexNextFrame];

                var refXMin = Math.min(refSamplePrevFrame[0],
                                      refSampleThisFrame[0],
                                      refSampleNextFrame[0]);

                var refYMin = Math.min(refSamplePrevFrame[1],
                                      refSampleThisFrame[1],
                                      refSampleNextFrame[1]);

                var refXMax = Math.max(refSamplePrevFrame[0],
                                      refSampleThisFrame[0],
                                      refSampleNextFrame[0]);

                var refYMax = Math.max(refSamplePrevFrame[1],
                                      refSampleThisFrame[1],
                                      refSampleNextFrame[1]);

                // Varience is expected to be at most 1 pixel beyond the range,
                // due to integer rounding of pixel position.
                var positionTolerance = 1; // 1 pixel

                maxVariance = Math.max(maxVariance,
                                       refXMin - sample[0] - positionTolerance,
                                       sample[0] - refXMax - positionTolerance,
                                       refYMin - sample[1] - positionTolerance,
                                       sample[1] - refYMax - positionTolerance);
              });

              is(maxVariance, 0, 'Smooth scroll animated position must not be '
                 + 'framerate dependent at deltaX: ' + deltaX + ', deltaY: '
                 + deltaY + ', frameRate: ' + frameRate + 'fps');
              testFrameRate();
            });
          } else {
            waitForAllPaintsFlushed(testDeltas);
          }
        }

        testFrameRate();

      });
    }

    testDeltas();

  }

  function sampleAnimation(startX, startY, endX, endY, timeStep, callback) {
    // The animation must be stopped at the destination position for
    // minStoppedFrames consecutive frames to detect that the animation has
    // completed.
    var minStoppedFrames = 15; // 15 frames

    // In case the simulation fails to converge, the test will time out after
    // processing maxTime milliseconds of animation.
    var maxTime = 10000; // 10 seconds

    var positionSamples = [];

    var frameCountAtDestination = 0;

    // Take control of refresh driver so we can synthesize
    // various frame rates
    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0);
    waitForAllPaintsFlushed(function() {

      window.scrollTo(startX, startY);
      window.scrollTo({left: endX, top: endY, behavior: 'smooth'});

      var currentTime = 0; // ms

      function advanceTime() {
        if(currentTime < maxTime && frameCountAtDestination < 15) {

          positionSamples.push([window.scrollX, window.scrollY]);

          currentTime += timeStep;
          SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(timeStep);
          waitForAllPaintsFlushed(function() {
            if (window.scrollX == endX && window.scrollY == endY) {
              frameCountAtDestination++;
            } else {
              frameCountAtDestination = 0;
            }

            advanceTime();
          });

        } else {
          isnot(frameCountAtDestination, 0,
                'Smooth scrolls must always end at their destination '
                + 'unless they are interrupted, at deltaX: '
                + (endX - startX) + ', deltaY: ' + (endY - startY));

          window.scrollTo(0, 0);

          // Release control of refresh driver
          SpecialPowers.DOMWindowUtils.restoreNormalRefresh();

          waitForAllPaintsFlushed(function() {

            // We must not include the duplicated frames at the animation
            // destination as the tests are dependant on the total duration of
            // the animation to be accurate.
            positionSamples.splice(1 - minStoppedFrames,
                                   minStoppedFrames - 1);

            callback(positionSamples);
          });
        }
      }

      advanceTime();

    });
  }

  </script>
</head>
<body>
<pre id="test">
</pre>

<div id="scroll_behavior_test_body">
      <div id="scroll_to_target" class="scroll_to_target"></div>
</body>
</html>
